#!/usr/bin/perl

use Device::SerialPort qw( :PARAM :STAT 0.07 );
use IPC::ShareLite;
use POSIX qw(strftime mktime);
use Storable qw( freeze thaw );
use DBI;
use Data::Dumper;

require "./settings.inc";

my $MAX_PROCESSED = 	2000;	# script will close after recording this number
#my $debug =		1;
my $STALL_DEFAULT=10; # how many seconds to wait for new input

my $last_processed_hour = time; $last_processed_hour -= (($last_processed % 3600) + 3600); # always process last hour on startup
my $last_processed_day = 0;

#
# open database
#
$dbh = DBI->connect("dbi:mysql:database=energy:host=localhost", $db_user, $db_pass) || die "Cannot connect to database: ".$DBI::errstr."\n";

#
# created shared memory segment
#
my $shared_mem = IPC::ShareLite->new(-key => 6666, -size => 131072, -create  => 'yes', -destroy => 'yes') or die $!;

#
# open serial port
#
$p = new Device::SerialPort ($serial_port, $quiet) || die "Can't open ".$serial_port.": $!\n";
$p->baudrate(9600);
$p->parity("none");
$p->databits(8);
$p->stopbits(1);
$p->read_char_time(0);     # don't wait for each character
$p->read_const_time(1000); # 1 second per unfulfilled "read" call

#
# get last readings
#
my $last_temp = 0;
my $last_watts = [0, 0, 0];
for my $devices (1..$MAX_DEVICES) {
	my $dbread = $dbh->selectrow_hashref("SELECT date_time,temperature,energy FROM instantaneous_readings WHERE device_id=$devices ORDER BY date_time DESC LIMIT 1");
	if (!defined $dbread) {
		printf STDERR "Device %d: No last reading found.  Assuming them to be zero.\n",$devices if $debug;
	} else {
		$last_temp = $dbread->{temperature};
		$last_watts[$devices-1] = $dbread->{energy};
		printf STDERR "Last energy reading for device %d: %s: energy %d W, %0.2f deg C\n", $devices, scalar localtime($dbread->{date_time}), $last_watts[$devices-1], $last_temp if $debug;
	}

	&update_hourly_table(time-(3*86400), $device);

}

my $this_temp = 0;
my @this_watts = [0, 0, 0];

my $timeout=$STALL_DEFAULT;
my $this_processed = 0;
my $latest_readings;
my $chars=0;
my $buffer="";
while (($timeout>0) && ($this_processed < $MAX_PROCESSED)) {
	my ($count,$saw)=$p->read(5000); # will read 5000 chars
	if ($count > 0) {
		$chars += $count;
		$buffer .= $saw;

# printf STDERR "got: '%s'\n",$saw; 
		if ($buffer =~ /<msg>.*<\/msg>/) {
			# got a full message, good..
			$this_processed++;

			# rotate recent history
			for ($i=($recent_history-1); $i > 0; $i--) {
				$latest_readings->[$i]->{timestamp} = $latest_readings->[$i-1]->{timestamp};
				$latest_readings->[$i]->{temp} = $latest_readings->[$i-1]->{temp};
				for ($j=0; $j < $MAX_DEVICES; $j++) {
					$latest_readings->[$i]->{energy}->[$j] = $latest_readings->[$i-1]->{energy}->[$j];
				}
			}
			$latest_readings->[0]->{timestamp} = time;

			# read temperature
			if ($buffer =~ /<tmpr>([0-9\.]+)<\/tmpr>/) {
				$latest_readings->[0]->{temp} = $1;
				printf STDERR "this_temp %0.4f last_temp %0.2f\n",$latest_readings->[0]->{temp},$last_temp if $debug;
			}

			# read temperature readings
			for my $devices (1..$MAX_DEVICES) {
				if ($buffer =~ /<ch$devices><watts>(\d+)<\/watts><\/ch$devices>/) {
					$latest_readings->[0]->{energy}->[$devices-1] = $1;
					printf STDERR "device %d: this_watts %d, last_watts %d\n",$devices,$latest_readings->[0]->{energy}->[$devices-1],$last_watts[$devices-1] if $debug;

					# only store reading in database if its passed the difference thresholds
					if (($latest_readings->[0]->{energy}->[$devices-1] >= ($last_watts[$devices-1] + $watt_threshold)) ||
					    ($latest_readings->[0]->{energy}->[$devices-1] <= ($last_watts[$devices-1] - $watt_threshold)) ||
					    ($latest_readings->[0]->{temp} >= ($last_temp + $temp_threshold)) ||
					    ($latest_readings->[0]->{temp} <= ($last_temp - $temp_threshold))) {
						printf STDERR "recording device %d: temp %0.2f last_temp %0.2f energy %d last_energy %d\n",$devices,$latest_readings->[0]->{temp},$last_temp,$latest_readings->[0]->{energy}->[$devices-1],$last_watts[$devices-1];

						$dbh->do("INSERT INTO instantaneous_readings (date_time,temperature,energy,device_id) VALUES (".$latest_readings->[0]->{timestamp}.",".$latest_readings->[0]->{temp}.",".$latest_readings->[0]->{energy}->[$devices-1].",".$devices.")");

						$last_watts[$devices-1] = $latest_readings->[0]->{energy}->[$devices-1];
						$last_temp = $latest_readings->[0]->{temp};
					}

					# always record 'latest' readings in shared memory
					$shared_mem->store(freeze($latest_readings));
				}
			}

			&update_hourly_table;

			$buffer = "";
		}
	}
}

#####################################################################
# this function takes instantaneous W readings and turns them into
# hourly Wh values.

sub update_hourly_table
{
	my $stime;
	my $now = time;
	my @now_t = localtime($now);
	my @old_t = localtime($last_processed_hour);

	if ($old_t[2] != $now_t[2]) {
		# if there has been an hour change then trigger an update as of last hour start
		$stime = POSIX::mktime(0, 0, $old_t[2], $old_t[3], $old_t[4], $old_t[5], $old_t[6], $old_t[7], $old_t[8]);
	} elsif (($now - $last_processed) > 300) {
		# if its been 5 minutes since we last did an update, do so now.
		# this means we can see the accumulating KWh for the current hour as it progresses
		$stime = POSIX::mktime(0, 0, $now_t[2], $now_t[3], $now_t[4], $now_t[5], $now_t[6], $now_t[7], $now_t[8]);
	} else {
		return; # do nothing
	}
	$last_processed_hour = $now;

	for my $device (1..$MAX_DEVICES) {
		$stime = $stime - ($stime % 3600); # normalize to within an hour..
		my $list_query = $dbh->prepare('SELECT date_time, energy, temperature FROM instantaneous_readings WHERE device_id = '.$device.' AND date_time >= '.$stime.' ORDER BY date_time ASC');
		die "SQL error: query returned error ".$list_query->errstr."\n" if (!($list_query->execute()));

		my $last_key = "";
		my $last_kwh = 0;
		my $last_time = 0;
		my $last_temp = 0;

		my $total_kwh_this_reading = 0;
		my $total_secs_this_reading = 0;
		my $total_temp_this_reading = 0;

		for (my $i=0; $i < $list_query->rows; $i++) {
			my $qr = $list_query->fetchrow_hashref();
			my @t = localtime($qr->{date_time});
			my $key = POSIX::mktime(0, 0, $t[2], $t[3], $t[4], $t[5], $t[6], $t[7], $t[8]);	# key is based on hour
			my $secs_to_add;

			goto SKIP if ($last_key eq "");
			if ($key ne $last_key) {
				# add final readings for the last hour
				$secs_to_add = $key - $last_time;
				for (my $j=0; $j < $secs_to_add; $j++) {
					$total_kwh_this_reading += $last_kwh;
					$total_kwh_this_reading += $last_kwh;
					$total_temp_this_reading += $last_temp;
					$total_secs_this_reading++;
				}

				$sql = sprintf "INSERT INTO hourly_readings (date_time,device_id,energy,temperature) VALUES (%d,%d,%0.4f,%0.2f) ".
					"ON DUPLICATE KEY UPDATE energy=%0.4f,temperature=%0.2f",
					$last_key, $device,
					($total_kwh_this_reading / $total_secs_this_reading), ($total_temp_this_reading / $total_secs_this_reading),
					($total_kwh_this_reading / $total_secs_this_reading), ($total_temp_this_reading / $total_secs_this_reading);
				$dbh->do($sql);

				# zero for next hour
				$total_kwh_this_reading = 0;
				$total_temp_this_reading = 0;
				$total_secs_this_reading = 0;
				$last_time = $key;
			}

			# add 'n' seconds to our running total of current hour
			$secs_to_add = $qr->{date_time} - $last_time;
			for (my $j=0; $j < $secs_to_add; $j++) {
				$total_kwh_this_reading += $last_kwh;
				$total_temp_this_reading += $last_temp;
				$total_secs_this_reading++;
			}

SKIP:
			$last_key = $key;
			$last_kwh = $qr->{energy};
			$last_temp = $qr->{temperature};
			$last_time = $qr->{date_time};
		}
	}
}

#####################################################################

